<?php
// +----------------------------------------------------------------------
// | INPHP
// | Copyright (c) 2023 https://inphp.cc All rights reserved.
// | Author: 幺月儿(https://gitee.com/lulanyin) Email: inphp@qq.com
// | 该文件的开源协议以所在项目的LICENSE文件为准，请遵守开源协议要求
// +----------------------------------------------------------------------
// | 解压脚本
// +----------------------------------------------------------------------
namespace app\fs\cmd;

use app\fs\model\FilesModel;
use Inphp\Core\Config;
use Inphp\Core\Db\Redis;
use Inphp\Core\Util\File;
use Inphp\Core\Util\Log;
use Inphp\Core\Util\Str;

class unzip
{
    /**
     * 文件ID
     * @var int
     */
    public int $fileId = 0;

    /**
     * 输出到控制台
     * @var bool
     */
    public bool $output = false;

    public function __construct(array $params = [])
    {
        $id = $params["id"] ?? 0;
        $this->fileId = is_numeric($id) && $id > 0 ? ceil($id) : 0;
        if ($this->fileId <= 0) {
            $this->debug("ID参数无效");
            return;
        }
        $this->output = $params["output"] ?? false;
    }

    public function run()
    {
        $file = FilesModel::emptyQuery()->where("id", $this->fileId)->first();
        if (empty($file)) {
            $this->debug("文件不存在");
            return;
        }
        if ($file["extension"] !== "zip") {
            $this->debug("非ZIP文件，无法解压");
            return;
        }
        //开始解压处理
        $this->unzip($file);
    }

    /**
     * 解压
     * @param array $file
     */
    private function unzip(array $file)
    {
        $attachment = Config::get("define.attachment");
        $src = $attachment.$file["src"];
        if (!is_file($src)) {
            $this->debug("文件已丢失，无法解压: {$src}");
            return;
        }
        $tempDir = $attachment."/unzip/{$file["id"]}";
        if (is_dir($tempDir)) {
            $this->debug("删除已存在的临时文件夹");
            if (!$this->removeDir($tempDir)) {
                $this->debug("删除临时文件夹失败，解压失败");
                return;
            }
        }
        if (!@mkdir($tempDir, 0777, true)) {
            $this->debug("创建临时文件夹失败，解压失败");
            return;
        }
        Redis::set("fs_unzip_{$this->fileId}", 2);
        $zip = new \ZipArchive();
        if ($zip->open($src) === true) {
            $filesLen = $zip->numFiles;
            $this->debug('共'.$filesLen.'个文件');
            for ($i = 0; $i < $filesLen; $i ++) {
                $statInfo = $zip->statIndex($i, \ZipArchive::FL_ENC_RAW);
                $encode = mb_detect_encoding($statInfo['name'], array("ASCII", "UTF-8", "GB2312", "GBK", "BIG5"));
                $name = $encode == "UTF-8" ? $statInfo['name'] : mb_convert_encoding($statInfo['name'], 'UTF-8', $encode);
                if ($encode != 'UTF-8') {
                    $this->debug('文件「'.($i + 1).'」名称需要转码');
                    $zip->renameIndex($i, $name);
                }
            }
            $zip->close();
            //重新打开
            $this->debug('重新打开ZIP');
            $zip->open($attachment.$file['src']);
            $this->debug('文件将先解压到临时文件夹');
            //解压
            $zip->extractTo($tempDir);
            //关闭
            $zip->close();
            //从文件夹中递归获取文件，然后保存
            $this->fetchDir($tempDir, $file['parentId'], $file['groupName'], $file['uid']);
        }
        //处理完成，删除临时文件夹
        @$this->removeDir($tempDir);
        Redis::del("fs_unzip_{$this->fileId}");
    }

    /**
     * 遍历文件夹保存
     * @param string $dir
     * @param int $parentId
     * @param string $groupName
     * @param int $uid
     */
    private function fetchDir(string $dir, int $parentId = 0, string $groupName = 'other', int $uid = 0)
    {
        $dh = opendir($dir);
        $attachment = Config::get("define.attachment");
        while ($file = readdir($dh))
        {
            if(stripos($file, ".") !== 0 && $file != '__MACOSX' && $file != 'DS_Store')
            {
                //为了防止中文名称乱码
                //$encode = mb_detect_encoding($file, array("ASCII", "UTF-8", "GB2312", "GBK", "BIG5"));
                $filename = $file;
                //
                $fullPath = $dir."/".$file;
                $this->debug($dir."/".$filename);
                if(is_dir($fullPath))
                {
                    //在当前目录下，创建一个文件夹，保存后得到ID，再进行下一步文件保存
                    //先判断，是否有重名文件夹，有的话直接覆盖式
                    $_db = FilesModel::emptyQuery()
                        ->where("parentId", $parentId)
                        ->where("name", $filename)
                        ->where("type", "dir");
                    if(is_numeric($groupName))
                    {
                        $_db->whereRaw("groupName='{$groupName}'");
                    }
                    else
                    {
                        $_db->where("groupName", $groupName);
                    }
                    $exists = $_db->first();
                    if(!empty($exists))
                    {
                        if ($exists["destroy"] == 1) {
                            //恢复
                            @FilesModel::emptyQuery()
                                ->where("id", $exists["id"])
                                ->update(["destroy" => 0]);
                        }
                        $this->fetchDir($fullPath, $exists['id'], $exists['groupName'], $uid);
                    }
                    else
                    {
                        $db = FilesModel::emptyQuery()
                            ->set([
                                'parentId'      => $parentId,
                                'groupName'     => $groupName,
                                'uid'           => $uid,
                                'name'          => $filename,
                                'src'           => "",
                                'hash'          => "dir",
                                'type'          => 'dir'
                            ]);
                        if($db->insert())
                        {
                            $this->fetchDir($fullPath, $db->getLastInsertId(), $groupName, $uid);
                        }
                    }
                }
                else
                {
                    $type = "file";
                    //由后缀进行文件格式判断
                    $suffix = strrchr($filename, ".");
                    if(!empty($suffix))
                    {
                        $type = in_array(substr($suffix, 1), explode(",", "jpg,png,jpeg,bpm,gif,psd,webp,psd")) ? "image" : (
                            in_array(substr($suffix, 1), explode(",", "mp4,avi,mov,flv,wmv,mkv")) ? "video" : "file"
                        );
                    }
                    $hash = md5_file($fullPath);
                    //查询
                    $exists = FilesModel::emptyQuery()->where("hash", $hash)->first();
                    if(empty($exists) || $exists['parentId'] != $parentId || $groupName != $exists['groupName'])
                    {
                        $src = !empty($exists) ? $exists['src'] : null;
                        if(empty($exists))
                        {
                            $saveDir = "/".$type."s/".date("Y/m/d");
                            if(!is_dir($attachment.$saveDir))
                            {
                                @File::makeDir($attachment.$saveDir);
                            }
                            $filename = mb_strlen($filename) > 100 ? mb_substr($filename, 0, 100) : $filename;
                            //将文件复制一份过来
                            $save_file_name = $this->makeFileName($attachment.$saveDir, $suffix);
                            @copy($fullPath, $attachment.$saveDir.'/'.$save_file_name);
                            $src = $saveDir.'/'.$save_file_name;
                        }
                        //保存多一份在当前
                        @FilesModel::emptyQuery()
                            ->insert([
                                "parentId"      => $parentId,
                                "groupName"     => $groupName,
                                "uid"           => $uid,
                                "name"          => $filename,
                                "extension"     => substr($suffix, 1),
                                "src"           => $src,
                                "hash"          => $hash,
                                "type"          => $type,
                                "size"          => $this->getFileSize($attachment.$src)
                            ]);
                    }
                    if (!empty($exists) && $exists["destroy"] == 1) {
                        //恢复
                        @FilesModel::emptyQuery()->where("id", $exists["id"])->update(["destroy" => 0]);
                    }
                }
            }
        }
        closedir($dh);
    }

    /**
     * 生成文件名
     * @param string $dir
     * @param string $suffix
     * @return string
     */
    private function makeFileName(string $dir, string $suffix): string
    {
        $name = time().Str::randomNumber().$suffix;
        return is_file($dir."/".$name) ? $this->makeFileName($dir, $suffix) : $name;
    }

    /**
     * 遍历删除文件夹
     * @param string $dir
     * @return bool
     */
    private function removeDir(string $dir): bool
    {
        $dh = opendir($dir);
        while ($file = readdir($dh)) {
            if($file != "." && $file != "..") {
                $full_path = $dir."/".$file;
                if(!is_dir($full_path)) {
                    unlink($full_path);
                } else {
                    $this->removeDir($full_path);
                }
            }
        }
        closedir($dh);
        if(rmdir($dir)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * 获取文件大小，单位KB
     * @param string $src
     * @return float|int
     */
    private function getFileSize(string $src): float|int
    {
        return is_file($src) ? round(filesize($src) / 1024, 2) : 0;
    }

    /**
     * Debug
     * @param string $text
     */
    private function debug(string $text)
    {
        $log = Config::get("define.logs")."/unzip_{$this->fileId}.txt";
        Log::writeToEnd($log, $text);
        if ($this->output) {
            echo $text.PHP_EOL;
        }
    }
}